package com.cyngn.chrono;
import com.cyngn.chrono.api.*;
import com.cyngn.chrono.config.ServerConfig;
import com.cyngn.chrono.data.SampleMapper;
import com.cyngn.chrono.storage.Bootstrap;
import com.cyngn.chrono.storage.StorageManager;
import com.cyngn.vertx.eventbus.EventBusTools;
import com.cyngn.vertx.web.RestApi;
import com.cyngn.vertx.web.RouterTools;
import com.datastax.driver.core.Cluster;
import com.englishtown.vertx.cassandra.impl.DefaultCassandraSession;
import com.englishtown.vertx.cassandra.impl.JsonCassandraConfigurator;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.eventbus.Message;
import io.vertx.core.http.HttpServer;
import io.vertx.core.json.JsonObject;
import io.vertx.core.shareddata.LocalMap;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.handler.LoggerHandler;
import io.vertx.ext.web.handler.sockjs.SockJSHandler;
import org.apache.commons.codec.digest.DigestUtils;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.List;
import java.util.Random;
/**
* Server, starts everything up.
*
* to build:
* ./gradlew clean shadowJar
*
* to run:
* java -jar build/libs/chrono-server-0.5.0-fat.jar -conf conf.json -instances 2
*
* to test:
* // the urls to hit
* curl -v "http://localhost:7345/api/v1/endpoints"
*
* // sample url, you'll need to use the key in the response from the previous request or grab from the startup log
* curl -v --header "X-API-Key": [shared_api_key]" "http://localhost:7345/api/v1/timing/static_cached?unit=mb%26size=1"
*
* @author truelove@cyngn.com (Jeremy Truelove) 9/5/14
*/
public class Server extends AbstractVerticle {
private static final Logger logger = LoggerFactory.getLogger(Server.class);
// local shared map, shared across all vert.x instances
private LocalMap<String, Long> sharedData;
private final String SHARED_DATA = "shared_data";
public static final String INITIALIZER_THREAD_KEY = "InitializerThread";
// needs to be static and initialized once to be shared across verticals
private final static String apiSharedSecret;
private ServerConfig svrConfig;
static {
// create a per startup shared key, that all config endpoints can return across verticle instances
apiSharedSecret = DigestUtils.sha512Hex(DateTime.now().toString() + new Random().nextFloat());
}
private HttpServer server;
private DefaultCassandraSession session;
private static String INITIALIZED_MSG = "server.initialized";
private StorageManager storageManager;
@Override
public void start(final Future<Void> startedResult) {
try {
JsonObject config = config();
svrConfig = new ObjectMapper().readValue(config.toString(), ServerConfig.class);
svrConfig.cassandra = config.getJsonObject("cassandra");
} catch (IOException e) {
logger.error("Failed to load the server config", e);
stop();
}
// attempt to become the initializer thread
sharedData = vertx.sharedData().getLocalMap(SHARED_DATA);
sharedData.putIfAbsent(INITIALIZER_THREAD_KEY, Thread.currentThread().getId());
// setup a cassandra connection and storage access
JsonCassandraConfigurator configurator = new JsonCassandraConfigurator(vertx);
session = new DefaultCassandraSession(Cluster.builder(), configurator, vertx);
session.onReady(result -> {
storageManager = new StorageManager(session, svrConfig);
// listen for the initialized message, the sending thread receives this also
EventBusTools.oneShotLocalConsumer(vertx.eventBus(), INITIALIZED_MSG, getStartupHandler());
if (isInitializerThread()) {
// initialize the test data
SampleMapper.getInstance();
try {
logger.info("Starting up server... on ip: {} port: {}",
InetAddress.getLocalHost().getHostAddress(), svrConfig.port);
logger.info("Using shared api key: {}", apiSharedSecret);
// make sure the basic test data is in place
new Bootstrap(storageManager, success -> {
// bootstrap is done let all threads know they can begin to listen on the server
if(success) { vertx.eventBus().publish(INITIALIZED_MSG, new JsonObject()); }
else { stop(); }
}, svrConfig.defaultTestBaseUrl);
} catch (UnknownHostException e) {
logger.error("Failed to get host ip address", e);
stop();
}
}
});
}
protected final boolean isInitializerThread() {
return sharedData.get(INITIALIZER_THREAD_KEY) == Thread.currentThread().getId();
}
private Router buildApi(Router router) {
// log all requests, and setup the shared api key checker in front of all REST calls
RouterTools.registerRootHandlers(router, LoggerHandler.create(), new SharedKeyHandler(apiSharedSecret));
List<RestApi> apis = Lists.newArrayList(
new HealthCheck(),
new Timing(storageManager),
new TestEndpoints(storageManager.batchStorage, apiSharedSecret),
new Report(storageManager.reportStorage)
);
// register rest endpoints
for (RestApi api : apis) {
api.init(router);
if (isInitializerThread()) { api.outputApi(logger); }
}
return router;
}
/**
* Handles starting the server listening on all interfaces
*
* @return a callback to hit when ready for the server to open up listening
*/
private Handler<Message<Object>> getStartupHandler() {
return message -> {
server = vertx.createHttpServer();
Router router = Router.router(vertx);
buildApi(router);
setupSockJSServer(router);
server.requestHandler(router::accept);
server.listen(svrConfig.port, "0.0.0.0", event -> {
if (event.failed()) {
logger.error("Failed to start server, error:", event.cause());
stop();
} else { logger.info("Thread: {} starting to handle request", Thread.currentThread().getId()); }
});
};
}
private void setupSockJSServer(Router router) {
SockJSHandler handler = SockJSHandler.create(vertx).socketHandler(new TimingStreaming(storageManager, apiSharedSecret));
router.route(TimingStreaming.ROOT_API + "/*").handler(handler);
}
@Override
public void stop() {
logger.info("Stopping the server");
try {
if(server != null) { server.close(); }
} finally {
// Only one thread can close the vertx as vertx is shared across all instances.
Long shutdownThreadId = sharedData.putIfAbsent("shutdown", Thread.currentThread().getId());
if (shutdownThreadId == null) {
vertx.close(event -> {
logger.info("Vertx instance closed");
System.exit(-1);
});
}
}
}
}